<?php

namespace Abo\Larasearch\V0\ElasticSearch;

/**
 * Class Grammar
 * Description: EsBuilder 转 ElasticSearch DSL 数组
 * @package Abo\Larasearch\ElasticSearch
 */
class Grammar
{
    /**
     * @var array
     */
    protected $selectComponents = [
        '_source' => 'columns',
        'index' => 'index',
        'type' => 'type',
        'query' => 'wheres',
        'aggs' => 'aggs',
        'sort' => 'orders',
        'from' => 'offset',
        'size' => 'limit',
        'scroll' => 'scroll',
    ];

    /**
     * @param EsBuilder $builder
     * @return int
     */
    public function compileOffset(EsBuilder $builder): int
    {
        return $builder->offset;
    }

    /**
     * @param EsBuilder $builder
     * @return int
     */
    public function compileLimit(EsBuilder $builder): int
    {
        return $builder->limit;
    }

    /**
     * @param EsBuilder $builder
     * @return string
     */
    public function compileScroll(EsBuilder $builder): string
    {
        return $builder->scroll;
    }

    /**
     * @param EsBuilder $builder
     * @return array
     */
    public function compileSelect(EsBuilder $builder)
    {
        $body = $this->compileComponents($builder);
        $index = array_pull($body, 'index');
        $type = array_pull($body, 'type');
        $scroll = array_pull($body, 'scroll');
        $params = ['body' => $body, 'index' => $index, 'type' => $type];
        if ($scroll) {
            $params['scroll'] = $scroll;
        }
        return $params;
    }

    /**
     * @param EsBuilder $builder
     * @param $id
     * @param array $data
     * @return array
     */
    public function compileCreate(EsBuilder $builder, $id, array $data): array
    {
        return array_merge([
            'id' => $id,
            'body' => $data
        ], $this->compileComponents($builder));
    }

    /**
     * @param EsBuilder $builder
     * @param $id
     * @param array $data
     * @return array
     */
    public function compileDocInsert(EsBuilder $builder, $id, array $data): array
    {
        return array_merge([
            'id' => $id,
            'body' => $data
        ], $this->compileComponents($builder));
    }

    /**
     * @param EsBuilder $builder
     * @param array $data
     * @return array
     */
    public function compileDocBulk(EsBuilder $builder, array $data, string $key = 'id'): array
    {
        foreach ( $data as $k2Doc => $v2Doc ) {
            $body[] = [
                'create' => [
                    '_index' => $builder->index,
                    '_type' => $builder->type,
                    '_id' => $v2Doc[ $key ]
                ]
            ];

            $body[] = array_combine( array_keys( $v2Doc ), array_values( $v2Doc ) );
        }

        return array_merge([
            'body' => $body
        ], $this->compileComponents($builder));
    }

    /**
     * @param EsBuilder $builder
     * @param $id
     * @param array $data
     * @return array
     */
    public function compileUpdate(EsBuilder $builder, $id, array $data): array
    {
        return array_merge([
            'id' => $id,
            'body' => ['doc' => $data]
        ], $this->compileComponents($builder));
    }

    /**
     * @param EsBuilder $builder
     * @param $id
     * @return array
     */
    public function compileDelete(EsBuilder $builder, $id): array
    {
        return array_merge([
            'id' => $id,
        ], $this->compileComponents($builder));
    }

    /**
     * @param EsBuilder $builder
     * @return array
     */
    public function compileAggs(EsBuilder $builder): array
    {
        $aggs = [];

        foreach ($builder->aggs as $field => $aggItem) {
            if (is_array($aggItem)) {
                $aggs = $aggItem;
            } else {
                $aggs[$field . '_' . $aggItem] = [$aggItem => ['field' => $field]];
            }
        }

        return $aggs;
    }

    /**
     * @param EsBuilder $builder
     * @return array
     */
    public function compileColumns(EsBuilder $builder): array
    {
        return $builder->columns;
    }

    /**
     * @param EsBuilder $builder
     * @return string
     */
    public function compileIndex(EsBuilder $builder): string
    {
        return is_array($builder->index) ? implode(',', $builder->index) : $builder->index;
    }

    /**
     * @param EsBuilder $builder
     * @return string
     */
    public function compileType(EsBuilder $builder): string
    {
        return $builder->type;
    }

    /**
     * @param EsBuilder $builder
     * @return array
     */
    public function compileOrders(EsBuilder $builder): array
    {
        $orders = [];

        foreach ($builder->orders as $field => $orderItem) {
            $orders[$field] = is_array($orderItem) ? $orderItem : ['order' => $orderItem];
        }

        return $orders;
    }

    /**
     * @param EsBuilder $builder
     * @return array
     */
    protected function compileWheres(EsBuilder $builder): array
    {
        $whereGroups = $this->wherePriorityGroup($builder->wheres);

        $operation = count($whereGroups) === 1 ? 'must' : 'should';

        $bool = [];

        foreach ($whereGroups as $wheres) {
            $must = [];

            foreach ($wheres as $where) {
                if ($where['type'] === 'Nested') {
                    $must[] = $this->compileWheres($where['query']);
                } else {
                    //$must[] = [$where['leaf'] => [$where['column'] => $where['value']]];
                    $must[] = $this->whereLeaf($where['leaf'], $where['column'], $where['operator'], $where['value'], $where['addition']);
                }
            }

            if (!empty($must)) {
                $bool['bool'][$operation][] = count($must) === 1 ? array_shift($must) : ['bool' => ['must' => $must]];
            }
        }

        return $bool;
    }

    /**
     * @param string $leaf
     * @param $column
     * @param string|null $operator
     * @param $value
     * @return array
     */
    protected function whereLeaf( string $leaf, $column, string $operator = null, $value, array $addition ): array
    {
        $ret2Return = [];
        switch ( $leaf ) {
            case 'term':
            case 'match':
                $ret2Return = [ $leaf => [
                    $column => $value
                ] ];
                break;
            case 'match_phrase':
                $ret2Return = [ $leaf => [
                    $column => $value
                ] ];
                break;
            case 'multi_match':
                $ret2Return = [ $leaf => [
                    'query' => $value,
                    'fields' => is_array( $column ) ? $column : [ strval( $column ) ],
                ] ];
                break;
            case 'range':
                $ret2Return = [ $leaf => [
                    $column => is_array( $value ) ? $value : [ $operator => $value ]
                ] ];
                break;
            case 'like':
                $ret2Return = [ 'match' => [
                    $column => [
                        "query" => strval( $value ),
                        'fuzziness' => isset( $addition[ 'fuzziness' ] ) ? $addition[ 'fuzziness' ] : 'AUTO',
                    ]
                ] ];
                isset( $addition[ 'prefix_length' ] ) && $ret2Return[ 'match' ][ $column ][ 'prefix_length' ] = $addition[ 'prefix_length' ];
                isset( $addition[ 'max_expansions' ] ) && $ret2Return[ 'match' ][ $column ][ 'max_expansions' ] = $addition[ 'max_expansions' ];
                /*
                $ret2Return = [ 'fuzzy' => [
                    $column => [
                        'value' => str_val( $value ),
                        'fuzziness' => isset( $addition[ 'fuzziness' ] ) ? $addition[ 'fuzziness' ] : 'AUTO',
                        'prefix_length' => isset( $addition[ 'prefix_length' ] ) ? $addition[ 'prefix_length' ] : mb_strlen( $value ),
                        'max_expansions' => isset( $addition[ 'max_expansions' ] ) ? $addition[ 'max_expansions' ] : 1,
                    ]
                ] ];
                */
                break;
        }

        return $ret2Return;
    }

    /**
     * @param array $wheres
     * @return array
     */
    protected function wherePriorityGroup(array $wheres): array
    {
        //get "or" index from array
        $orIndex = (array)array_keys(array_map(function ($where) {
            return $where['boolean'];
        }, $wheres), 'or');

        $lastIndex = $initIndex = 0;
        $group = [];
        foreach ($orIndex as $index) {
            $group[] = array_slice($wheres, $initIndex, $index - $initIndex);
            $initIndex = $index;
            $lastIndex = $index;
        }

        $group[] = array_slice($wheres, $lastIndex);

        return $group;
    }

    /**
     * @param EsBuilder $query
     * @return array
     */
    protected function compileComponents(EsBuilder $query): array
    {
        $body = [];

        foreach ($this->selectComponents as $key => $component) {
            if (!empty($query->$component)) {
                $method = 'compile' . ucfirst($component);

                $body[is_numeric($key) ? $component : $key] = $this->$method($query, $query->$component);
            }

            if ( 'limit' == $component && is_int( $query->$component ) ) {
                $method = 'compile' . ucfirst($component);

                $body[is_numeric($key) ? $component : $key] = $this->$method($query, $query->$component);
            }
        }

        return $body;
    }
}